#include <stdio.h>
#include <math.h>
#include <string.h>

#include "pub_lib.h"

//if print debug infomation
#define DEBUG_VER

struct triangle {
	Quaternion v0, v1, v2; //init position of triangle apex
	Quaternion axis; //rotate axis
	int is_z_axis; //if rotate axis is z, set 1
	Vector2 vn0, vn1, vn2; //used to draw triangle
};

Quaternion x_axis = { 1.0, 0.0, 0.0, 0.0};
Quaternion y_axis = { 0.0, 1.0, 0.0, 0.0};
Quaternion z_axis = { 0.0, 0.0, 1.0, 0.0};

const int screenWidth = 700;
const int screenHeight = 700;

/*
 * default: rotate axis is z
 * float axis[3];
 * if rotate axis is z, is_z_axis = 1
 * otherwise is_z_axis = 0
 * */
void set_triangle_rotate_axis(struct triangle *ptri, Quaternion axis, int is_z_axis)
{
	ptri->axis = axis;
	ptri->is_z_axis = is_z_axis;
}


/* init triangle with apex */
void init_triangle(struct triangle *ptri, Quaternion v0, Quaternion v1, Quaternion v2)
{
	ptri->v0 = v0;
	ptri->v1 = v1;
	ptri->v2 = v2;

	set_triangle_rotate_axis(ptri, z_axis, 1);
}

/*
 *  1, 0, 0, -eye_pos[0],
    0, 1, 0, -eye_pos[1],
    0, 0, 1, -eye_pos[2],
    0, 0, 0, 1;

 * */
Matrix get_view_matrix(Quaternion eye_pos)
{
	Matrix result = { 1.0f, 0.0f, 0.0f, -eye_pos.x,
		0.0f, 1.0f, 0.0f, -eye_pos.y,
		0.0f, 0.0f, 1.0f, -eye_pos.z,
		0.0f, 0.0f, 0.0f, 1.0f };

	return result;
}

/*
  cos(radian), -sin(radian), 0, 0,
  sin(radian), cos(radian), 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1;

MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, rotation_angle * DEG2RAD);
Rotation around z only
 * */
Matrix get_model_matrix(float rotation_angle)
{
	float cosv, sinv;
	float angle;

	angle = rotation_angle * DEG2RAD;
	cosv = cosf(angle);
	sinv = sinf(angle);

	Matrix result = {
		cosv, -sinv, 0.0f, 0.0f,
		sinv, cosv, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f };

	return result;
}

/*
 * MatrixRotate(axis, angle * DEG2RAD);
 *
 * rotate by any axis
 * Rodrigues rotation formula
 *
 * rotation by 'angle' around axis 'n'
 * R(n, angle) = cos(angle)*I + (1 - cos(angle)) * n * nT
 *	+ sin(angle) * N
 * */
Matrix get_model_matrix_ex(Quaternion axis, float angle)
{
	float x = axis.x, y = axis.y, z = axis.z;
    float length = sqrtf(x*x + y*y + z*z);

    if ((length != 1.0f) && (length != 0.0f))
    {
        length = 1.0f/length;
        x *= length;
        y *= length;
        z *= length;
    }

    float sinres = sinf(angle * DEG2RAD);
    float cosres = cosf(angle * DEG2RAD);
    float t = 1.0f - cosres;

	Matrix result = { 0 };
	result.m0  = x*x*t + cosres;
    result.m1  = y*x*t + z*sinres;
    result.m2  = z*x*t - y*sinres;
    result.m3  = 0.0f;

    result.m4  = x*y*t - z*sinres;
    result.m5  = y*y*t + cosres;
    result.m6  = z*y*t + x*sinres;
    result.m7  = 0.0f;

    result.m8  = x*z*t + y*sinres;
    result.m9  = y*z*t - x*sinres;
    result.m10 = z*z*t + cosres;
    result.m11 = 0.0f;

    result.m12 = 0.0f;
    result.m13 = 0.0f;
    result.m14 = 0.0f;
    result.m15 = 1.0f;

	print_mat("model_ex", result);
	return result;
}

/*
 * MatrixPerspective(eye_fov * DEG2RAD, aspect_ratio, zNear, zFar);
 ensure zNear and zFar is negative value
 * */
Matrix get_projection_matrix(float eye_fov, float aspect_ratio,
                        float zNear, float zFar)
{
	float val, top, bottom, left, right;
	float rl, tb, fn;
	Matrix result = {0};

	val = tan(eye_fov * 0.5 * DEG2RAD);
	top = fabsf(zNear) * val;
	bottom = -top;
	right = aspect_ratio * top;
	left = -right;

	rl = right - left;
    tb = top - bottom;
    fn = zFar - zNear;

	result.m0 = zNear / right;
    result.m1 = 0.0f;
    result.m2 = 0.0f;
    result.m3 = 0.0f;

    result.m4 = 0.0f;
    result.m5 = zNear / top;
    result.m6 = 0.0f;
    result.m7 = 0.0f;

    result.m8 = 0.0f;
    result.m9 = 0.0f;
    result.m10 = -(zFar + zNear)/fn;
    result.m11 = -1.0f;

    result.m12 = 0.0f;
    result.m13 = 0.0f;
    result.m14 = -(zFar*zNear*2.0f)/fn;
    result.m15 = 0.0f;

	return result;
}

/* viewport transformation
 * save final coordinates to struct triangle
 * */
void viewport_trans(Quaternion point, Vector2* new, int width, int height)
{
	Quaternion v;
	float w;

	//print_qm("homogenous", point);
	//homogenous coordinates to orthonormal coordinates
	w = point.w;
	v.x = point.x / w;
	v.y = point.y / w;
	v.z = point.z / w;
	v.w = 1.0f;
	//print_qm("orthonormal", v);

	//viewport transformation
	point.x = 0.5 * width * (1.0 - v.x);
	point.y = 0.5 * height * (v.y + 1.0);

	//save new coordinates
	new->x = point.x;
	new->y = point.y;
}

/*
 * according to rotation angle, update the MVP matrix
 * and triangle apex
 * */
void update_triangle_mvp(struct triangle* ptri, float angle, Quaternion eye_pos)
{
	Matrix model, view, projection;
	Matrix pv, mvp;
	Quaternion newV0, newV1, newV2;

	//calc transformation M, V, P
	if (ptri->is_z_axis) {
		model = get_model_matrix(angle);
	} else {
		model = get_model_matrix_ex(ptri->axis, angle);
	}
	view = get_view_matrix(eye_pos);
	projection = get_projection_matrix(45, 1, -0.1, -50);

	//calc MVP: projection - view - Module
	pv = matrxiMul(projection, view); //pv = P * V
	mvp = matrxiMul(pv, model); //MVP = PV * M = P * V * M

	//transform points
	newV0 = QuaternionTransform(ptri->v0, mvp);
	newV1 = QuaternionTransform(ptri->v1, mvp);
	newV2 = QuaternionTransform(ptri->v2, mvp);

	//Viewport transformation
	viewport_trans(newV0, &ptri->vn0, screenWidth, screenHeight);
	viewport_trans(newV1, &ptri->vn1, screenWidth, screenHeight);
	viewport_trans(newV2, &ptri->vn2, screenWidth, screenHeight);

#ifdef DEBUG_VER
	print_mat("model", model);
	print_mat("view", view);
	print_mat("projection", projection);
	print_mat("mvp", mvp);
#endif
}

struct triangle tri;
int main(int argc, char** argv)
{
	// Initialization
	char key;

	float angle = 0.0;
	int change = 0;

	//apex of a triangle
	Quaternion v0 = {2.0, 0.0, -2.0, 1};
	Quaternion v1 = {0.0, 2.0, -2.0, 1};
	Quaternion v2 = {-2.0, 0.0, -2.0, 1};

	Quaternion eye_pos = {0, 0, 5, 1};
	//--------------------------------------------------------------------------------------
	init_triangle(&tri, v0, v1, v2);
	update_triangle_mvp(&tri, angle, eye_pos);

	InitWindow(screenWidth, screenHeight, "homework1 - rotate triangle");
	SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
	//--------------------------------------------------------------------------------------

	// Main game loop
	while (!WindowShouldClose())    // Detect window close button or ESC key
	{
		// Update
		//----------------------------------------------------------------------------------
		// TODO: Update your variables here
		key = (char)GetKeyPressed();

		if (key == 'A') {
			change = 1;
			angle += 15;
		} else if (key == 'D') {
			change = 1;
			angle -= 15;
		} else if (key == 'X') {
			change = 1;
			set_triangle_rotate_axis(&tri, x_axis, 0);
        } else if (key == 'Y') {
			change = 1;
			set_triangle_rotate_axis(&tri, y_axis, 0);
        } else if (key == 'Z') {
			change = 1;
			set_triangle_rotate_axis(&tri, z_axis, 1);
        }else if (key == ESC_KEY) {
			break;
		}

		if (change) {
			change = 0;
			printf("angle = %d\n", (int)angle);
			//update vn0, vn1, vn2 according to angle
			update_triangle_mvp(&tri, angle, eye_pos);
		}


		//----------------------------------------------------------------------------------

		// Draw
		//----------------------------------------------------------------------------------
		BeginDrawing();
		ClearBackground(RAYWHITE);
		DrawTriangleLines(tri.vn0, tri.vn1, tri.vn2, DARKBLUE);
		EndDrawing();
		//----------------------------------------------------------------------------------
	}

	// De-Initialization
	//--------------------------------------------------------------------------------------
	CloseWindow();        // Close window and OpenGL context
	//--------------------------------------------------------------------------------------

	return 0;
}
